#
# Package.
#

package iSOCK::Model::Specification;

#
# Parent.
#

use Moose;
use namespace::autoclean;

BEGIN { extends 'iSOCK::Model::Root' }

no Moose;

#
# Dependencies.
#

use SOCK::ATP;
use SOCK::BOT qw( gctcbtn );
use SOCK::DOM::Design;
use SOCK::LOG;
use SOCK::SVN::Product;
use SOCK::XLS;

use DateTime;
use File::Find;
use File::Slurp qw( slurp );
use Try::Tiny;

#
# Documentation.
#

=head1 NAME

iSOCK::Model::Specification - Catalyst Model

=head1 DESCRIPTION

Catalyst Model.

=head1 AUTHOR

Xavier Caron

=head1 LICENSE

This library is free software. You can redistribute it and/or modify
it under the same terms as Perl itself.

=cut

#
# Functions.
#

sub delete_session_files {
  my ( $self, $option ) = @_;
  
  my $session      = $option->{c}->session;
  my $session_path = $session->{session_path};
  my $product_name = $session->{product_name};
  
  my $count;
  
  no warnings 'uninitialized';
  
  $count += unlink grep { -f } map { glob "$session_path/$product_name$_" } qw( .mif );
  $count += unlink grep { -f } map { glob "$session_path/$_"              } qw( 4_SPIRIT*_out/*.spirit 5_*_out/* );
  
  $count;
}

sub session_keys {
  qw( ekb_file mif_stem mif_file spirit_file specification_version svn_product_revision ), map { "specification_product_$_" } qw( vendor library name version );
}

sub to_spirit {
  my ( $self, $option ) = @_;
  
  my ( $c, $specification_file, $log ) = ( $option->{c}, $option->{specification_file}, $option->{log} );
  
  my $session      = $c->session;
  my $session_path = $session->{session_path};
  
  my ( $svn_product, $svn_branch, $svn_tag ) = map { $session->{$_} } qw( svn_product svn_branch svn_tag );
  
  my $command = "sock_specification_to_spirit --indent 2 --file_indent 0 " .
    ( $specification_file ?
      "--specification $specification_file" :
      "--product $svn_product" . ( $svn_branch ? " --branch $svn_branch" : "" ) . ( $svn_tag ? " --tag $svn_tag" : "" )
    ) .
    ( $session->{ekb_file} ? ' --ekb ' . $session->{ekb_file} : '' )
  ;

  if ( $svn_product ) {
    if ( my $specification_r = $c->model ( 'App::Specification' )->find ( $svn_product ) ) {
      if ( my $repository_r = $specification_r->repository ) {
        $command .= " --repository_url '" . $repository_r->url . "'";

        if ( my $login = $repository_r->login ) {
          $command .= " --repository_login '$login'";
        }
        if ( my $password = $repository_r->password ) {
          $command .= " --repository_password '$password'";
        }
      }
    }
  }

  $session->{mif_file} = $specification_file || "$svn_product.mif";
  
  if ( $self->spawn ( $c, $command, $log ) ) {
    my $stem;

    if ( open my $fh, '<', "$session_path/sock_specification_to_spirit.log" ) {
      my $ko;
      
      while ( <$fh> ) {
        $ko = 1 if m/^-E-/;
        
        if ( m/ \s+ Product \s+ (vendor|library|name|version) \s+ : \s+ (.+) $/x ) {
          $session->{"specification_product_$1"} = $2 eq 'not found' ? undef : $2;
        }
        elsif ( m/ \s+ Specification \s+ version \s+ : \s+ (.+) $/x ) {
          $session->{specification_version}      = $1 eq 'not found' ? undef : $1;
        }
        elsif ( m/Serializing DOM into '4_SPIRIT_out\/([\w-]+).spirit' SPIRIT file/ ) {
          $stem = $1;
        }
      }

      close $fh;

      unless ( $ko ) {
        $session->{mif_stem}    = $stem;
        $session->{spirit_file} = "4_SPIRIT_out/$stem.spirit";

        return 1;
      }
    }
    else {
      $log->yell ( "Problem during invocation." );
    }
  }
  
  $self->delete_session_keys ({ c => $c });
  
  return;
}

sub component_lineup {
  my ( $self, $option ) = @_;
  
  my ( $c, $specification_id, $product_id, $specification_branch ) = ( $option->{c}, $option->{specification_id}, $option->{product_id}, $option->{specification_branch} );
  
  my $session      = $c->session;
  my $session_path = $session->{session_path} || '/tmp';
  
  my $log = SOCK::LOG->new ({ file => "$session_path/component_lineup.log" });
  
  my $svn_option = {};
  
  if ( my $specification_r = $c->model ( 'App::Specification' )->find ( $specification_id ) ) {
    $svn_option->{repository_url} = $specification_r->repository->url      if $specification_r->repository->url;
    $svn_option->{login}          = $specification_r->repository->login    if $specification_r->repository->login;
    $svn_option->{password}       = $specification_r->repository->password if $specification_r->repository->password;
  }
  
  my $svn = SOCK::SVN::Product->new ( $svn_option );

  my ( @components, @devices, $line_to_devices, @errors, $svn_revision );

  {
    $log->say_n_right ( "Checking out '$specification_id' product specification..." );

    my $mif_file = "$session_path/$specification_id.mif";
    unlink $mif_file if -f $mif_file;

    my $svn_url;

    if ( $svn->export_view ({ name => $specification_id, view => 'specification', path => $session_path, branch => $specification_branch }) && -f $mif_file ) { # , tag => $option{tag}
      ( $svn_url, $svn_revision ) = ( $svn->svn_url, $svn->revision );

      $log->say ( "Checked out '$svn_url' (r$svn_revision) file." );
      $log->left_n_say ( "Done." );
    }
    else {
      $log->yell ( "Could not check out any '$mif_file' MIF file!" );
      $log->left_n_say ( "Failed!" );

      push @errors, "Could not check out any '$mif_file' MIF file!";
    }

    if ( my $design_id = SOCK::DOM::Design->new ({ file => $mif_file, log => $log, no_semantic_check => 1 }) ) {
      if ( $design_id->ko ) {
        push @errors, "Found errors in MIF file!";
      }
      else {
        my $component;
        
        $component->{$_->{name}} = $_ for _component_loh ( $design_id ), map { _component_loh ( $_ ) } @{$design_id->subsystem_ids};
        $component->{$_->{name}} = $_ for $design_id->libraries_items;
        
        push @components, map { $component->{$_} } sort keys %$component;
        
        @devices         = $design_id->family_member_names;
        $line_to_devices = $design_id->series_loh;
      }
    }
    else {
      push @errors, "Could not parse '$mif_file' MIF file!";
    }
  }
  
  {
    my $followup_id = $specification_id;
    
    $log->say_n_right ( "Checking out '$followup_id' product follow-up..." );

    my $xls_file = "$session_path/$followup_id.xls";
    unlink $xls_file if -f $xls_file;

    my $svn_url;

    if ( $svn->export_view ({ name => $followup_id, view => 'followup', path => $session_path, branch => $specification_branch }) && -f $xls_file ) { # , tag => $option{tag}
      ( $svn_url, $svn_revision ) = ( $svn->svn_url, $svn->revision );

      $log->say ( "Checked out '$svn_url' (r$svn_revision) file." );
      $log->left_n_say ( "Done." );

      if ( my $xls_id = SOCK::XLS->new ({ file => $xls_file }) ) {
        $log->say_n_right ( "Building components..." );
        
        for my $component ( sort keys %{$xls_id->{component}} ) {
          my $number   = $xls_id->{component}->{$component}->{number};
          my $revision = $xls_id->{component}->{$component}->{revision};
          
          if ( my $datasheet_r = $c->model ( 'App::Component' )->search ({ id => { like => "%_$number" } })->first ) { # FIXME care about collisions?
            push @components, {
              type    => 'datasheet',
              name    => $datasheet_r->id,
              version => $revision,
            };
          }
          else {
            if ( my $atp_id = SOCK::ATP->new ({ number => $number, log => $log }) ) {
              $atp_id->show;

              push @components, {
                type        => 'datasheet',
                name        => $atp_id->stem_name,
                version     => $revision,
                description => $atp_id->part_number,
                url         => $atp_id->url,
              };
            }
            else {
              $log->yell ( "Could not find any '$number' document on ATP!" );
            }
          }
        }
        
        $log->left_n_say ( "Done." );
      }
      else {
        push @errors, "Could not parse '$xls_file' XLS file!";
      }
    }
    else {
      $log->yell ( "Could not check out any '$xls_file' MIF file!" );
      $log->left_n_say ( "Failed!" );
    }
  }

  if ( @errors ) {
    die "@errors";
  }
  elsif ( @components ) {
    return {
      product_id             => $product_id,
      product_scope          => 'specification',
      specification_id       => $specification_id,
      specification_url      => $svn->url ({ name => $specification_id, view => 'root' }),
      specification_revision => $svn_revision,
      components             => \@components,
      devices                => \@devices,
      line_to_devices        => $line_to_devices,
    };
  }

  return;
}

sub _component_loh {
  my ( $design_id ) = @_;
  
  return
    map { { name => _fix_specification_component_name ( $_->[2] ), version => $_->[3], type => 'ip' } }
    grep { $_->[2] !~ m/^(?: S?RAM | ROM )$/x }
    map { [ $design_id->instance_implementation_component_vlnv ({ instance => gctcbtn ( $_ => 'spirit:instanceName' ) }) ] }
    $design_id->findnodes ( '//spirit:componentInstance' );
}

sub _fix_specification_component_name { # FIXME from SOCK config instead?
  my ( $name ) = @_;
  
  {                                        # SAMA5 SAM4S
    CHIPID            => 'CHIP_ID',        # -     x
    EMACB             => 'ETHERNET_MACB',  # x     -
    HMATRIX64         => 'HMATRIX2',       # x     -
    PIO3A             => 'PIO3',           # x     -
    TDES              => 'DES3',           # x     -
    TSMC_FUSEBIT_CTRL => 'TSMC_FUSE_CTRL', # x     -
    USB_DEVICE_HS     => 'HUSB2DEV',       # x     -
  }->{$name} || $name;
}

sub integrate {
  my ( $self, $option ) = @_;
  
  my ( $c, $specification_id, $dummy, $test, $changed, $branch ) = ( $option->{c}, $option->{specification_id}, $option->{dummy}, $option->{test}, $option->{changed}, $option->{branch} );
  
  if ( my $specification_r = $c->model ( 'App::Specification' )->find ( $specification_id ) ) {
    my $session      = $c->session;
    my $session_path = $self->session_path ( $c );

    my $log = SOCK::LOG->new ({ file => "$session_path/isock_specification.log" });

    $log->say_n_right ( "Processing within " . $c->sessionid . " session..." );

    #
    # Arkham.
    #

    my $tat = $c->model ( 'App' )->specification_ahead_datasheets_as_tat ( $specification_id );

    {
      $log->say_n_right ( "Checking datasheet status..." );

      if ( $tat ) {
        $log->counsel ( "Ahead of last published revision:" );
        $log->mumble  ( $tat );
      }

      $log->left_n_say ( "Done." );
    }

    #
    # Job.
    #

    my $job_r;
    
    try {
      $job_r = $c->model ( 'App::Job' )->create ({
        specification_id => $specification_id,
        when_started     => DateTime->now,
        session_id       => $c->sessionid,
        status_id        => 'running',
        sock_version     => $SOCK::VERSION,
        isock_version    => $iSOCK::VERSION,
      });
      
      $job_r->update ({ specification_branch => $branch }) if defined $branch;
      $job_r->update ({ dummy                => 1       }) if         $dummy;  # 't' 'true' 'y' 'yes' 'on' '1'
    }
    catch {
      $c->log->error ( $_ );
    };

    #
    # Spawn.
    #
    
    my $command = "sock_integrate_product --product $specification_id --dwim --commit"
    . (         $dummy   ? " --dummy"              : "" )
    . ( defined $branch  ? " --branch '$branch'"   : "" )
    . (         $changed ? " --changed '$changed'" : "" )
    ;

    if ( my $repository_r = $specification_r->repository ) {
      $command .= " --repository_url '" . $repository_r->url . "'";
      
      if ( my $login = $repository_r->login ) {
        $command .= " --repository_login '$login'";
      }
      if ( my $password = $repository_r->password ) {
        $command .= " --repository_password '$password'";
      }
    }
    
   #$specification_r->update ({ busy => 1 });
    
    if ( $self->spawn ( $c, $command, $log, { AVR_DEVICES_ROOT => $c->config->{directory}->{adu} } ) ) {
      $specification_r->update ({ busy => 0 });
      
      $session->{integrate_specification} = 1;

      my ( $ko, $ide_changed );

      if ( -f "$session_path/sock_integrate_product.log" ) {
        my $slurped = slurp ( "$session_path/sock_integrate_product.log" );
        
        if ( grep { m/ -E- \s /x } $slurped ) {
          $ko = 1;
          
          $log->left_n_say ( "Failed!" );
        }
        else {
          $ide_changed = grep { m/ \b IDE \s+ : \s+ OK \b /x } $slurped;
          
          $log->left_n_say ( "Done." );
        }
      }
      else {
        $log->yell ( my $message = "Could not find any 'sock_integrate_product.log' file." );
        $log->left_n_say ( "Failed!" );
        
        die $message; # FIXME is this ever supposed to happen?
      }

      $log->close;
      
      if ( $ide_changed ) {
        try {
#          use SOCK::JRA;
#          
#          my $jra_id = SOCK::JRA->new ({ url => $c->config->{harvest_url} });
#          
#          my $harvest_r = $c->model ( 'App::Harvest' )->create ({ job_id => $job_r->id, status_id => 'dispatched' });
#          
#          $jenkins->build ({ token => $c->config->{harvest_token}, parameters => [
#            HARVEST_ID       => $harvest_r->id,
#            HARVEST_URL      => $specification_r->repository->url . "/$specification_id/" . ( defined $branch ? "branches/$branch" : "trunk" ) . "/integration",
#            SPECIFICATION_ID => $specification_id,
#          ] });
          
          $c->model ( 'Mail' )->send ({ c => $c, mail => {
            Subject => "[iSOCK] done with specification ($specification_id) [HARVEST]",
            Data    => "HARVEST_URL => " . $specification_r->repository->url . "/$specification_id/" . ( defined $branch ? "branches/$branch" : "trunk" ) . "/integration" . "\n",
          } });
        }
        catch {
          $c->log->error ( $_ );
        };
      }

      my @attachments = (
        [ "$specification_id/sock_specification_to_spirit.log" => 'text/plain' ],
        [ "$specification_id/sock_commit_integration.log"      => 'text/plain' ],
      );

      {
        open my $fh, '<', "$session_path/sock_integrate_product.log";

        push @attachments,
          map { [ "$specification_id/$_" => 'text/plain' ] }
          grep { defined }
          map { m/ - \s+ (3_LOGs_out\/sock_followup_to_whatever-\w+\.log) \s+ \(\d+\) /x ? $1 : undef }
          <$fh>
        ;
      }

      #
      # Mail.
      #

      my $user_ids = $test ?
        [ $c->config->{root_id} ]
      : [ $c->model ( 'App' )->specification_mail_user_ids ({ specification_id => $specification_id }) ]
      ;

      $c->model ( 'Mail' )->done ({
        with        => 'specification',
        topic       => $specification_id,
        user_ids    => $user_ids,
        attachments => \@attachments,
        c           => $c,
      });

      #
      # Job.
      #

      my $slurped = slurp ( "$session_path/sock_integrate_product.log" );

      try {
        if ( -f "$session_path/$specification_id/sock_specification_to_spirit.log" ) {
          my $slurped_too = slurp ( "$session_path/$specification_id/sock_specification_to_spirit.log" );

          if ( my ( $revision ) = ( $slurped_too =~ / \(r(\d+)\) file\./ ) ) {
            $job_r->update ({ specification_revision => $revision });
          }

          $job_r->update ({ specification_status_id => $slurped_too =~ /-E-/ ? 'failed' : 'done' });
        }

        if ( $job_r->specification_status_id eq  'done' ) {
          if ( -f "$session_path/$specification_id/sock_followup_to_whatever.log" ) {
            my $slurped_too = slurp ( "$session_path/$specification_id/sock_followup_to_whatever.log" );

            if ( my ( $revision ) = ( $slurped_too =~ / \(r(\d+)\) file\./ ) ) {
              $job_r->update ({ followup_revision => $revision });
            }

            $job_r->update ({ followup_status_id => ( $slurped_too =~ /-E-/ || $slurped_too =~ /\b KO \b/x ) ? 'failed' : 'done' });
          }

          if ( $job_r->followup_status_id ne 'failed' ) {
            for ( glob "$session_path/$specification_id/3_LOGs_out/sock_followup_to_whatever-*.log" ) {
              my $slurped_too = slurp ( $_ );

              next unless $slurped_too =~ /-E-/;

              $job_r->update ({ followup_status_id => 'failed' });

              last;
            }

            if ( my ( $revision ) = ( $slurped =~ /sock_commit_integration --revision (\d+)/ ) ) {
              $job_r->update ({ commit_revision => $revision });
            }
          }
        }

        my $status;
        if ( $job_r->specification_status_id eq 'failed' || $job_r->followup_status_id eq 'failed' ) {
          $status = 'failed';
        }
        else {
          $status = ( $slurped =~ /-E-/ ? 'failed' : 'done' );
        }

        {
          my $main_log;

          if ( $tat ) {
            $main_log =
              $log->massage ( 'I' => "Checking datasheet status...",      0 )
            . $log->massage ( 'W' => "Ahead of last published revision:", 1 )
            . $log->massage ( '-' => $tat,                                1 )
            . $log->massage ( 'I' => "Done.",                             0 )
            ;
          }

          $main_log .= $slurped;

          $job_r->update ({ status_id => $status, when_completed => DateTime->now, main_log => $main_log });
        }
      }
      catch {
        $c->log->error ( $_ );
      };
      
      return $slurped;
    }
    else {
      $log->left_n_say ( "Failed!" );

      try {
        $job_r->update ({ status_id => 'failed', when_completed => DateTime->now });
      }
      catch {
        $c->log->error ( $_ );
      };

      die "Problem during invocation.";
    }
  }
  else {
    die "Unkown '$specification_id' specification.";
  }

  return;
}

sub milestone {
  my ( $self, $option ) = @_;
  
  my ( $user_id, $specification_id, $milestone, $c ) = ( $option->{user_id}, $option->{specification_id}, $option->{milestone}, $option->{c} );
  
  if ( my $svn = SOCK::SVN::Product->new ) {
    ( my $now = DateTime->now ) =~ s/[-:]//g;
    
    my $tag       = $milestone . "-" . $now;
    my $trunk_url = $svn->url ({ name => $specification_id, view => 'none'              });
    my $tag_url   = $svn->url ({ name => $specification_id, view => 'none', tag => $tag });
    
    my ( $message, $tarball );
    
    if ( my $session_path = $c->session->{session_path} ) {
      my $stem      = "$specification_id-$tag";
      my $directory = "$session_path/$stem";
      
      if ( -d $directory ) {
        system "rm -rf $directory";
      }

      if ( my $url = $svn->url ({ name => $specification_id, view => 'integration', file => '5_HTML_out' }) ) {
        $c->log->info ( "Exporting $url URL..." );
        $message .= "Exported $url URL.\n";
        $svn->export ({ url => $url, into => $directory });
        $c->log->info ( "Done." );
      }
      if ( my $url = $svn->url ({ name => $specification_id, view => 'integration', file => '5_IDE_out' }) ) {
        $c->log->info ( "Exporting $url URL..." );
        $message .= "Exported $url URL.\n";
        $svn->export ({ url => $url, into => $directory });
        $c->log->info ( "Done." );
      }
      
      if ( -d "$directory/5_IDE_out" && -d "$directory/5_HTML_out" ) {
        {
          my ( $year ) = 1900 + ( localtime ( time ) )[5];

          my $wanted = sub {
            if ( -f ) {
              local $^I   = '';
              local @ARGV = ( $_ );

              while ( <> ) {
                s/\%copyright_year\%/$year/g;

                print;
              } continue {
                close ARGV if eof;
              }
            }
          };

          File::Find::find ( { wanted => $wanted }, $directory );
        }
        
        $tarball = "$session_path/$stem.tar.bz2";
        
        $c->log->info ( "Creating $stem.tar.bz2 tarball..." );
        system "tar cfj $tarball -C $session_path $stem\n";
        $c->log->info ( "Done." );
        
        $message .= "Created $stem.tar.bz2 tarball.\n";
      }
      
      if ( -d $directory ) {
        system "rm -rf $directory";
      }
    }
    
    my $entity;
    
    try {
      my $option = {
        subcommand     => 'copy',
        url            => $trunk_url,
        target_url     => $tag_url,
        message        => "reached $milestone milestone $now ($user_id)",
        options        => "--username " . $c->config->{subversion_user},
        return_command => 1,
        dummy          => 1, # FIXME eventually remove this line
      };
      
      if ( my $sub_command = $svn->svn ( $option ) ) {
        $message .= "Should have created $tag tag.\n"; # FIXME alter when dummy above is nuked
        
        $tarball =~ s/^root//;
        
        $entity = { message => $message, milestone => $tag, tarball => $tarball };
      }
    };
    
    unless ( $entity ) {
      die "Problem during commit.";
    }
    
    return $entity;
  }
  else {
    die "Could not create SOCK::SVN::Product instance.";
  }
}

#
# True.
#

1;
